package com.gitee.wsl.common.ui.ext


import androidx.compose.runtime.MutableState
import androidx.compose.runtime.Stable
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.unit.IntOffset
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.Saver
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.util.packFloats
import androidx.compose.ui.util.unpackFloat1
import androidx.compose.ui.util.unpackFloat2
import com.gitee.wsl.common.ui.base.Point
import com.gitee.wsl.ext.base.ifValue
import com.gitee.wsl.io.dataproxy.DataGet
import com.gitee.wsl.math.radiansToDegrees
import com.gitee.wsl.math.degreesToRadians
import com.gitee.wsl.mathematics.function.scale.Scales
import com.gitee.wsl.mathematics.function.scale.continuous.linear.LinearScale
import com.gitee.wsl.mathematics.interpolator.Interpolate
import com.gitee.wsl.mathematics.interpolator.UnInterpolatorFun
import com.gitee.wsl.mathematics.interpolator.Percent
import com.gitee.wsl.unit.angle.deg
import com.gitee.wsl.mathematics.interpolator.pct
import com.gitee.wsl.mathematics.vector.vec2.Vec2f
import com.gitee.wsl.mathematics.vector.vec2.Vector2
import kotlin.math.PI
import kotlin.math.abs
import kotlin.math.absoluteValue
import kotlin.math.acos
import kotlin.math.atan2
import kotlin.math.cos
import kotlin.math.floor
import kotlin.math.max
import kotlin.math.min
import kotlin.math.roundToInt
import kotlin.math.sin
import kotlin.math.sqrt
import kotlin.random.Random


@Stable
fun Offset(x: Double, y: Double) = Offset(x.toFloat(), y.toFloat())

//fun Offset.toIntOffset() = IntOffset(x.toInt(),y.toInt())

fun Offset.rotate90() = Offset(-y, x)


// Treat offset as a vector
//fun Offset.normalize(): Offset {
//    val l = 1.0f / length()
//    return Offset(x * l, y * l)
//}

fun Offset.length(): Float {
    return sqrt(x * x + y * y)
}

val Offset.Companion.PointComparatorX : Comparator<Offset>
    get() = Comparator<Offset> { a, b -> a.x.compareTo(b.x) }

val Offset.Companion.PointComparatorY : Comparator<Offset>
    get() = Comparator<Offset> { a, b -> a.x.compareTo(b.x) }

fun Offset.Companion.fromAnAngle(angle: Float, length: Float = 1f): Offset {
    return Offset(length * cos(angle), length * sin(angle))
}

fun Offset.Companion.randomVector(): Offset {
    return fromAnAngle((Random.nextFloat() * PI * 2).toFloat())
}

val Offset.calcDegree get() = (atan2(y, x) * 180 / PI).toFloat().let { deg ->
    if (deg < 0) { deg + 360 }
    else { deg }
}.deg

/*operator fun Offset.minus(other: Offset): Offset {
    return Offset(this.x - other.x,this.y - other.y)
}

operator fun Offset.plus(other: Offset): Offset {
    return Offset(this.x + other.x,this.y + other.y)
}*/
operator fun Offset.div(value:Number) = Offset(x / value.toFloat(), y / value.toFloat())

/**
 * Negate a Offset
 *
 * @return the negative value of this Offset
 */
/*operator fun Offset.unaryMinus(): Offset {
    return Offset(-this.x, -this.y)
}*/

//fun Offset.toPointF(): PointF = PointF(x,y)

/*Below are few simple vector algebra methods*/

fun Offset.add(other: Offset): Offset {
    return Offset(this.x + other.x,    this.y + other.y)
}

operator fun Offset.plusAssign(other: Offset) {
    this.add(other)
}

/**
 * To add a vector to [this] vector and multiplies the [other] vector with [scalar]
 */
fun Offset.addWithScalarMultiply(other: Offset, scalar: Float): Offset {
    return Offset(other.x * scalar,other.y * scalar)
}

/**
 * To subtract a vector from [this] vector
 */
fun Offset.sub(other: Offset): Offset {
    return Offset(this.x - other.x,    this.y - other.y)
}

operator fun Offset.minusAssign(other: Offset) {
    this.sub(other)
}

/**
 * To multiply [this] vector with [x] factor and [y] factor
 */
fun Offset.multiply(x: Float, y: Float): Offset {

    return Offset(  this.x * x,this.y * y)
}

/**
 * To multiply [this] vector with single [factor]
 * See also [scalarMultiply]
 */
fun Offset.multiply(factor: Float): Offset {
    return multiply(factor, factor)
}

operator fun Offset.timesAssign(factor: Float) {
    this.multiply(factor)
}

/**
 * To divide [this] vector by [x] and [y] values
 */
fun Offset.div(x: Float, y: Float): Offset {
    return Offset( this.x / x,this.y / y)
}

/**
 * To divide [this] vector by single [factor]
 */
fun Offset.divide(factor: Float): Offset {
    return div(factor, factor)
}

operator fun Offset.divAssign(factor: Float) {
    this.divide(factor)
}

/**
 * Calculates the magnitude (length) of the vector and returns the result asa float
 */
fun Offset.mag(): Float = sqrt(this.magSq())

/**
 * Calculates the squared magnitude of [this] vector and returns the result as a float
 */
fun Offset.magSq(): Float = this.x * this.x + this.y * this.y

/**
 * Calculates the dot product of two vectors
 */
fun Offset.dot(other: Offset): Float = this.x * other.x + this.y * other.y



/**
 * Normalizes the vector to length 1 - making unit vector
 *
 * @return [Offset]
 */
fun Offset.normalize(): Offset {
    val len = this.mag()
    if (len != 0f) {
        return this.scalarMultiply(1 / len)
    }
    return this
}

/**
 * Set the magnitude of this vector to the value used for the <b>n</b>
 * parameter.
 *
 * @param n the new length of the vector
 */
fun Offset.setMag(n: Float): Offset {
    return this.normalize().multiply(n)
}

/**
 * Limit the magnitude of this vector to the value used for the <b>max</b>
 * parameter.
 * <code>
 * val v = Offset(10, 20);
 * // v has components [10.0, 20.0]
 * v.limit(5);
 * // v's components are set to
 * // [2.2271771, 4.4543543 ]
 * </code>
 */
fun Offset.limit(max: Float): Offset {
    val magSq = this.magSq()
    if (magSq > max * max) {
        val norm = sqrt(magSq)
        return this.div(norm, norm).scalarMultiply(max)
    }
    return this
}

/**
 * Calculate the angle of rotation for this vector(only 2D vectors).
 */
fun Offset.heading(): Float {
    return atan2(this.y, this.x).radiansToDegrees()
}

/**
 * Rotate the vector to a specific angle, magnitude remains the same
 * @param angle - Angle in radians
 */
fun Offset.setHeading(angle: Float): Offset {
    val mag = this.mag()
    return Offset( mag * cos(angle),mag * sin(angle))
}

/**
 * Rotates [this] vector by given [angle]
 */
fun Offset.rotate(angle: Float): Offset {
    val newHeading = (this.heading() + angle).degreesToRadians()
    val mag = this.mag()
    return Offset(cos(newHeading) * mag, sin(newHeading) * mag)
}

/**
 * Sets [this] vector as [other] vector
 */
/*fun Offset.set(other: Offset): Offset {
    this.x = other.x
    this.y = other.y
    return this
}*/

/**
 * Returns angle between [this] vector and [other] vector
 *
 * @return Angle in radians
 */
fun Offset.angleBetween(other: Offset): Float {
    val dotmag = this.dot(other) / (this.mag() * other.mag())
    val angle = acos(min(1f, max(-1f, dotmag)).toDouble()).toFloat()
    // angle = angle * Math.signum(this.c)
    return angle.degreesToRadians()
}

/**
 * Linear interpolate the vector to another vector
 * @param x the x component
 * @param y the y component
 * @param amt the amount of interpolation; some value between 0.0
 *                     (old vector) and 1.0 (new vector). 0.9 is very near
 *                      the new vector. 0.5 is halfway in between.
 */
fun Offset.lerp(x: Float, y: Float, amt: Float): Offset {

    return Offset(this.x + (x - this.x) * amt,this.y + (y - this.y) * amt)
}

/**
 * Reflect the incoming vector about a normal to a line in 2D, or about a normal to a plane in 3D
 * This method acts on the vector directly
 * @param surfaceNormal the [Offset] to reflect about, will be normalized by this method
 */
fun Offset.reflect(surfaceNormal: Offset): Offset {
    surfaceNormal.normalize()
    return this.sub(surfaceNormal.scalarMultiply(2 * this.dot(surfaceNormal)))
}

fun Offset.scalarMultiply(scalar: Float): Offset {
    return Offset(this.x * scalar,this.y * scalar)
}

/**
 * Multiply every value in the Offset by a given factor
 *
 * @param factor the factor to use
 * @return the multiplied Offset
 */
operator fun Offset.times(factor: Number): Offset {
    return Offset(factor.toFloat() * this.x, factor.toFloat() * this.y)
}


/**
 * Increments [this] vector by [factor]
 */
operator fun Offset.inc(factor: Float): Offset {

    return Offset(this.x + factor,    this.y + factor)
}

/**
 * Decrements [this] vector by [factor]
 */
operator fun Offset.dec(factor: Float): Offset {

    return Offset(this.x - factor, this.y - factor)
}

fun Offset.toFloatArray() = floatArrayOf(x,y)

/**
 * Coerce an [Offset] x value in [horizontalRange] and y value in [verticalRange]
 */
fun Offset.coerceIn(
    horizontalRange: ClosedRange<Float>,
    verticalRange: ClosedRange<Float>
) = Offset(this.x.coerceIn(horizontalRange), this.y.coerceIn(verticalRange))

fun Offset.size() = Size(x, y)
fun Offset.absoluteValue() = Offset(x.absoluteValue, y.absoluteValue)


fun Interpolate.Companion.interpolatePoint(start: Offset, end: Offset): Interpolate<Offset> {
    val diff = end - start
    return Interpolate{ percent -> start + diff * percent.value }
}

// TODO ?
//fun interpolateRound(start: Double, end: Double): Interpolator<Double> {
//    val diff = end - start
//    return { percent -> round(start + percent.value * diff) }
//}

// TODO : should do uninterpolate (precise only) / uninterpolateX (on x-axis) / uninterpolateY (on y-axis)
fun Interpolate.Companion.uninterpolatePointOnX(start: Offset, end: Offset): UnInterpolatorFun<Offset> {
    val diff = end.x - start.x + 0.0
    return if (diff != .0) { offset -> Percent((offset.x - start.x) / diff) } else { _ -> 0.pct }
}

fun Interpolate.Companion.uninterpolatePointOnY(start: Offset, end: Offset): UnInterpolatorFun<Offset> {
    val diff = end.y - start.y + 0.0
    return if (diff != .0) { offset -> Percent((offset.y - start.y) / diff) } else { _ -> 0.pct }
}

fun Scales.Continuous.vector(init: LinearScale<Point>.() -> Unit = {}): LinearScale<Point> =
    LinearScale(Interpolate.Companion::interpolatePoint, Interpolate.Companion::uninterpolatePointOnX, Point.PointComparatorX).apply(init)

fun snapshotStateOffsetSaver() = Saver<MutableState<Offset>, Long>(
    save = { state -> packFloats(state.value.x, state.value.y) },
    restore = { value -> mutableStateOf(Offset(unpackFloat1(value), unpackFloat2(value))) }
)


/**
 * Return true if the point is locked
 * @param dragOffset Tapped offset
 * @param xOffset in the X axis
 */
fun Offset.isDragLocked(dragOffset: Float, xOffset: Float) =
    ((dragOffset) > x - xOffset / 2) && ((dragOffset) < x + xOffset / 2)


/**
 * Return true if the point is selected
 * @param tapOffset Tapped offset
 * @param xOffset in the X axis
 * @param bottom bottom Value
 */
fun Offset.isTapped(tapOffset: Offset, xOffset: Float, bottom: Float, tapPadding: Float) =
    ((tapOffset.x) > x - (xOffset + tapPadding) / 2) && ((tapOffset.x) < x + (xOffset + tapPadding) / 2) &&
            ((tapOffset.plus(Offset(0f, tapPadding))).y > y) && ((tapOffset.y) < bottom)


/**
 * Returns true if the tapped point is withing the given boundries else false
 * @param tapOffset Tapped offset
 * @param tapPadding plus or minus padding from the point or clickable padding
 */
fun Offset.isPointTapped(tapOffset: Offset, tapPadding: Float) =
    ((tapOffset.x) > x - tapPadding) && ((tapOffset.x) < x + tapPadding) &&
            ((tapOffset.plus(Offset(0f, tapPadding))).y > y) &&
            ((tapOffset.minus(Offset(0f, tapPadding))).y < y)


/**
 * Return true if the point is selected
 * @param tapOffset Tapped offset
 * @param yOffset in the Y axis
 * @param left left Value
 * @param tapPadding plus or minus padding from the point or clickable padding
 * @param xAxisWidth width of horizontal bar
 */
fun Offset.isYAxisTapped(
    tapOffset: Offset,
    yOffset: Float,
    left: Float,
    tapPadding: Float,
    xAxisWidth: Float
) =
    ((tapOffset.y) < y + (yOffset + tapPadding) / 2) && ((tapOffset.y) > y - (yOffset + tapPadding) / 2) &&
            ((tapOffset.plus(Offset(tapPadding, 0f))).x < xAxisWidth) && ((tapOffset.x) > left)

/**
 * Return true if the point is selected
 * @param tapOffset Tapped offset
 * @param barWidth width of the bar
 * @param barHeight height of the bar
 * @param tapPadding plus or minus padding from the point or clickable padding
 */
fun Offset.isStackedBarTapped(
    tapOffset: Offset,
    barWidth: Float,
    barHeight: Float,
    tapPadding: Float
) = ((tapOffset.x) > x - (barWidth + tapPadding) / 2) && ((tapOffset.x) < x + (barWidth + tapPadding) / 2) &&
        (tapOffset.y > y) && ((tapOffset.y) < barHeight)

fun Offset.toIntOffset(): IntOffset {
    return IntOffset(x.roundToInt(), y.roundToInt())
}

/**
 * Floors an [Offset] into an [IntOffset], taking the [floor] of both coordinates.
 */
fun floor(offset: Offset): IntOffset = IntOffset(floor(offset.x).toInt(), floor(offset.y).toInt())

fun Offset.toVec2f() = Vec2f(OffsetDataGet(this))

class OffsetDataGet(val offset: Offset, override val size: Int=2):DataGet<Float>{
    override fun get(index: Int): Float {
        return (index == 0).ifValue(offset.x ,offset.y)
    }

    override fun values(): List<Float> = listOf(offset.x,offset.y)
}

fun Vector2<Float,*>.toOffset() = Offset(x, y)

/**
 * Inverts the X and Y coordinates of a given offset
 */
 inline val Offset.inverted: Offset get() = Offset(-x, -y)

/**
 * Inverts the X coordinate of a given offset
 */
 inline val Offset.invertedX: Offset get() = copy(x = -x)

/**
 * Inverts the Y coordinate of a given offset
 */
 inline val Offset.invertedY: Offset get() = copy(x = -x)

/**
 * Offset expressed as a fraction of the size of the widget.
 */
 typealias FractionalOffset = Offset

/**
 * Converts fractional offset to absolute offset
 */
 fun FractionalOffset.asAbsolute(size: Size): Offset = Offset(x * size.width, y * size.height)

/**
 * Converts absolute offset to fractional offset
 */
 fun Offset.asFractional(size: Size): FractionalOffset = Offset(x / size.width, y / size.height)

/**
 * Normalization of coordinates to the lower left point
 * Normalization is necessary because the scaling calculation is based on the fact that the origin point
 * of the coordinates is in the lower left corner
 */
fun Offset.normalizeCentroid(size: IntSize): Offset = copy(
    y = abs(y - size.height),
)

fun Offset.normalizePan(): Offset = copy(
    x = -x,
)

fun Offset.coerceIn(rect: Rect): Offset {
    val xOffset =
        when {
            x < rect.left -> rect.left
            x > rect.right -> rect.right
            else -> x
        }
    val yOffset =
        when {
            y < rect.top -> rect.top
            y > rect.bottom -> rect.bottom
            else -> y
        }
    return Offset(xOffset, yOffset)
}
